In [45]:
def test_neighbour_squares():
actual = neighbour_squares(0, 0, 1, 1)
expected = []
assert actual == expected
actual2 = neighbour_squares(0, 0, 2, 1)
expected2 = [(0, 1)]
assert actual2 == expected2, "jklj" #str(actual2)
actual3 = neighbour_squares(0, 0, 1, 2)
expected3 = [(1, 0)]
assert actual3 == expected3
actual4 = sorted(neighbour_squares(0, 0, 2, 2))
expected4 = sorted([(0, 1), (1, 0), (1, 1)])
assert actual4 == expected4
actual5 = sorted(neighbour_squares(1, 1, 4, 4))
expected5 = sorted([(0, 0), (1, 0), (0, 1), (2, 2), (1,2), (2,1), (0,2), (2,0)])
assert actual5 == expected5
def test_count_bombs():
b = [["B", "_", "B"],
["_", "B", "B"]]
assert count_bombs(0, 0, b) == 1
assert count_bombs(1, 2, b) == 2
assert count_bombs(1, 0, b) == 4
b2 = [["B"],
["-"],
["B"]]
assert count_bombs(0, 0, b2) == 0
assert count_bombs(0, 1, b2) == 2
b3 = [["B","-","-","B","B","-"]]
assert count_bombs(0, 0, b3) == 0
assert count_bombs(2, 0, b3) == 1
print("Testing Complete")
In [47]:
def neighbour_squares(x, y, num_rows, num_cols):
"""
(x, y) 0-based index co-ordinate pair.
num_rows, num_cols: specifiy the max size of the board
returns all valid (x, y) coordinates from starting position.
"""
offsets = [(-1,-1), (-1,0), (-1,1),
( 0,-1), ( 0,1),
( 1,-1), ( 1,0), ( 1,1)]
result = []
for x2, y2 in offsets:
px = x + x2
py = y + y2
row_check = 0 <= px < num_rows
col_check = 0 <= py < num_cols
if row_check and col_check:
point = (py, px)
result.append(point)
return result
def count_bombs(x, y, board):
"""
returns the number of neighbours of (x,y) that are bombs. Max is 8, min is 0.
"""
num_rows = len(board[0])
num_cols = len(board)
squares = neighbour_squares(x, y, num_rows, num_cols)
bombs_found = 0
for px, py in squares:
if board[px][py] == "B":
bombs_found += 1
return bombs_found
# Testing...
test_neighbour_squares()
test_count_bombs()
'neighbour_squares' takes an (x, y) pair co-ordinate and returns the neighbours of that square. Often a square has eight neighbors (up left, up right, below, below right, etc), however squares on the edge of the board have fewer. The purpose of "row_check" and "col_check" is to help avoid possible index errors later on and also avoids a possible bug where the index is negative. For example:
[1,2,3]
[4,5,6]
[7,8,9]
square(3) has an index of (0,2). If we subtract 1 from both sides we get (-1,1). And the index (-1,1) is equivalent to (2,1), thus we end saying the neighbour of square(3) is square(8). This is only desirable in the cases where we want the edges of the board to 'wrap round'.
Carving this logic out into its own function (as opposed to doing everything in the count_bombs function) allows us to be flexible and this code can be reused (notice that "neighbour_squares" doesn't even take a board as an argument).
The 'count_bombs' function takes an (x,y) position and a board. If then returns how many neighbour squares are in fact bombs. This function can however be improved.
You may remember me saying in the design lecture that its usually better to give a function a name that describes what it does and not what it is used for. This functions name ("count_bombs") makes this mistake. To fix this problem, we have to ask ourselves:
"What this function is actually doing?"
Here's what it is not doing: counting_bombs. A bomb in our current implementation is simply the string "B". So what this function is actually doing is counting how many occurrences of the string "B" there are. That's not a very useful function. A much more useful function would be to count any arbitrary character. So let's change the function name and signature to better reflect this new understanding
def count_occurence_of_character_in_neighbour_squares(x, y, array, character):
...
And thats a function that we could use easily in a chess game:
ENEMY_PIECE = "P"
chess_board = ["_, "P", _]
["k", "P, _]
def can_king_capture_piece(king_position_x, king_position_y, chess_board):
possible_captures = count_occurence_of_character_in_neighbour_squares(king_position_x,
king_position_y,
chess_board,
ENEMY_PIECE)
return possible_captures.
And it is also a function we could use in a sudoku game to check if a 3x3 grid is correct (or not):
def is_3x3_correct(mid_point_x, mid_point_y, grid):
for number in range(1, 10):
if count_occurence_of_character_in_neighbour_squares(mid_point_x, mid_point_y, grid, str(number)) != 1:
return False # number is either missing or there are duplicates
return True
Hopefully those two additional examples illustrate how writing flexible code is useful. By thinking about that the function does (as opposed to concentrating on how we use it) we were able to make the code significantly more flexible with minimal work. And it doesn't take that much effort to imagine scenarios where we might want to use such a function.
The final improvement I can think of that is worth mentioning is that in the previous homework we actually wrote helper functions that could get a value at a given square. Since we already have that function we may as well use it.
In [ ]:
def get_square(x, y, board):
"""
This function takes a board and returns the value at that square(x,y).
"""
return board[x][y]
def count_occurence_of_character_in_neighbour_squares(x, y, board, character):
"""
returns the number of neighbours of (x,y) that are bombs. Max is 8, min is 0.
"""
num_rows = len(board[0])
num_cols = len(board)
squares = neighbour_squares(x, y, num_rows, num_cols)
character_found = 0
for px, py in squares:
square_value = get_square(px, py, board)
if square_value == character:
character_found += 1
return character_found